

#include "buffered_write_sequencer.h"
#include "display_options.h"
#include "image_source.h"
#include "iterm2_canvas.h"
#include "kitty_canvas.h"
#include "renderer.h"
#include "term_query.h"
#include "terminal_canvas.h"
#include "thread_pool.h"
#include "tv_help.h"
#include "tv_time.h"
#include "tv_version.h"
#include "unicode_block_canvas.h"
#include "utils.h"

#ifdef WITH_TV_SIXEL
#include "sixel_canvas.h"
#endif

// To display version number
#ifdef WITH_TV_OPENSLIDE_SUPPORT
#include "openslide_source.h"
#endif
#ifdef WITH_TV_VIDEO
#include "video_source.h"
#endif
#ifdef WITH_TV_GRPAPHICSMAGICK
#include <Magick++.h>

#include "graphics_magick_source.h"
#endif
#ifdef WITH_TV_SIXEL
#include <sixel.h>
#endif
#ifdef WITH_TV_RSVG
#include <cairo-version.h>
#include <librsvg/rsvg.h>
#endif
#ifdef WITH_TV_POPPLER
#include <cairo-version.h>
#include <poppler.h>
#endif

#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <inttypes.h>
#include <libswscale/swscale.h> // Only needed for version.
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <fstream>
#include <future>
#include <iostream>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <vector>

#ifndef TV_VERSION
#define TV_VERSION "(0.0.1)"
#endif

using tv::Duration;
using tv::ImageSource;
using tv::ITerm2GraphicsCanvas;
using tv::KittyGraphicsCanvas;
using tv::rgba_t;
using tv::TerminalCanvas;
using tv::Time;
using tv::UnicodeBlockCanvas;

enum class ExitCode {
  kSuccess = 0,
  kImageReadError = 1,
  kParameterError = 2,
  kNotATerminal = 3,
  kCantOpenOutput = 4,
  kFilelistProblem = 5,
  // Keep in sync with error codes mentioned in manpage
};

enum class Pixelation {
  kNotChosen,
  kHalfBlock,
  kQuarterBlock,
  kKittyGraphics,
  kiTerm2Graphics,
#ifdef WITH_TV_SIXEL
  kSixelGraphics,
#endif
};

enum class ClearScreen {
  kNot,
  kBeforeFirstImage,
  kBeforeEachImage,
};

namespace tv {
// Options configuring how images/videos are arranged and presented.
struct PresentationOptions {
  // Rendering
  Pixelation pixelation = Pixelation::kNotChosen;
  bool sixel_cursor_workaround = false;
  bool tmux_workaround = false;
  bool terminal_use_upper_block = false;
  bool use_256_color = false; // For terminals that don't do 24 bit color

  // Arrangement
  int grid_cols = 1; // Grid arrangement
  int grid_rows = 1;

  // Per image
  Duration duration_per_image = Duration::InfiniteFuture();
  int loops = tv::kNotInitialized; // If animation: loop count through all
  bool hide_cursor = true;         // Hide cursor while emitting image
  ClearScreen clear_screen = ClearScreen::kNot; // Clear between images ?
  Duration duration_between_images; // How long to wait between images
  Duration duration_for_row;        // Wait time for a grid row.
};
} // namespace tv

// Image sources; as future as they are being filled while we start presenting
typedef std::vector<std::future<tv::ImageSource *>> LoadedImageSources;

// Use most cores that are available.
static const int kDefaultThreadCount =
    std::max(1, 3 * (int)std::thread::hardware_concurrency() / 4);

volatile sig_atomic_t interrupt_received = 0;
static void InterruptHandler(int signo) { interrupt_received = 1; }

static int usage(const char *progname, ExitCode exit_code, int width,
                 int height) {
#ifdef WITH_TV_VIDEO
  static constexpr char kFileType[] = "image/video";
#else
  static constexpr char kFileType[] = "image";
#endif
  const char *after_slash_progname = rindex(progname, '/');
  progname = after_slash_progname ? after_slash_progname + 1 : progname;
  const char *const default_title = getenv("TV_DEFAULT_TITLE");
  fprintf(stderr, "usage: %s [options] <%s> [<%s>...]\n", progname, kFileType,
          kFileType);
  fprintf(
      stderr,
      "\e[1mOptions\e[0m (most common first):\n"
      "\t-p<pixelation> : Pixelation: 'h' = half blocks    'q' = quarter "
      "blocks\n"
      "\t                             'k' = kitty graphics 'i' = iTerm2 "
      "graphics\n"
#ifdef WITH_TV_SIXEL
      "\t                             's' = sixel graphics\n"
#endif
      "\t                 Default: Auto-detect graphics, otherwise "
      "'quarter'.\n"
      "\t--grid=<cols>[x<rows>] : Arrange images in a grid (\"contact "
      "sheet\").\n"
      "\t-C, --center   : Center image horizontally in available cell.\n"
      "\t--title[=<fmt_str>]: Print title above each image. Accepts the "
      "following\n"
      "\t                 format parameters: %%f = full filename; %%b = "
      "basename\n"
      "\t                                    %%w = image width; %%h = "
      "height\n"
      "\t                                    %%D = internal decoder used\n"
      "\t                 If no parameter is given, defaults to \"%%f\"\n"
      "\t                 Env-var override with TV_DEFAULT_TITLE%s%s%s\n"
      "\t-f<filelist>   : Read newline-separated list of image files to "
      "show.\n"
      "\t                 Relative filenames are relative to current "
      "directory.\n"
      "\t                 (-f and -F can be provided multiple times.)\n"
      "\t-F<filelist>   : like -f, but relative filenames considered "
      "relative\n"
      "\t                 to the directory containing the filelist.\n"
      "\t-b<str>        : Background color to use behind alpha channel. "
      "Format\n"
      "\t                 color name like 'yellow', '#rrggbb', 'auto' or "
      "'none'.\n"
      "\t                'auto' is terminal background color. "
      "(default 'auto').\n"
      "\t-B<str>        : Checkerboard pattern color to use on alpha.\n"
      "\t--pattern-size=<n> : Integer factor scale of the checkerboard "
      "pattern.\n"
      "\t--auto-crop[=<pre-crop>] : Crop away all same-color pixels around "
      "image.\n"
      "\t                 The optional pre-crop is the width of border to\n"
      "\t                 remove beforehand to get rid of an uneven border.\n"
      "\t--rotate=<exif|off> : Rotate according to included exif "
      "orientation.\n"
      "\t                      or 'off'. Default: exif.\n"
      "\t-W, --fit-width: Scale to fit width of available space, even if it\n"
      "\t                 exceeds height.\n"
      "\t-U, --upscale[=i]: Allow Upscaling. If an image is smaller than "
      "the\n"
      "\t                 available frame (e.g. an icon), enlarge it to "
      "fit.\n"
      "\t                 Optional parameter 'i' only enlarges in integer "
      "steps.\n"
      "\t--clear[=every]: Clear draw area first. Optional argument 'every' "
      "will\n"
      "\t                 clear before every image (useful with -w/-wr,\n"
      "\t                 but not with --grid)\n"
#ifdef WITH_TV_VIDEO
      "\t-V             : Directly use Video subsystem. Don't probe image\n"
      "\t                 decoding first (useful, if you stream video from "
      "stdin)\n"
      "\t-I             : Only  use Image subsystem. Don't attempt video "
      "decoding\n"
#endif
      "\t-w<seconds>    : Wait time between images (default: 0.0).\n"
      "\t-wr<seconds>   : like above, but wait time between rows in grid.\n"
      "\t-a             : Switch off anti-aliasing (default: on).\n"
      "\t-g<w>x<h>      : Output geometry in character cells. Partial "
      "geometry\n"
      "\t                 leaving out one value -g<w>x or -gx<h> is "
      "possible,\n"
      "\t                 the other value it then derived from the terminal "
      "size.\n"
      "\t                 Default derived from terminal size is %dx%d\n"
      "\t-o<outfile>    : Write to <outfile> instead of stdout.\n"
      "\t-E             : Don't hide the cursor while showing images.\n"
      "\t--compress[=level]: Only for -pk or -pi: Compress image data. More\n"
      "\t                 CPU use, but less used bandwidth. (default: 1)\n"
      "\t--threads=<n>  : Run image decoding in parallel with n threads\n"
      "\t                 (Default %d, 3/4 #cores on this machine)\n"
      "\t--color8       : Choose 8 bit color mode for -ph or -pq\n"
      "\t--version      : Print detailed version including used libraries.\n"
      "\t                 (%s)\n"
      "\t--verbose      : Print some stats after images shown.\n"
      "\t-h             : Print this help and exit.\n"
      "\t--help         : Page through detailed manpage-like help and exit.\n"

      "\n  \e[1mScrolling\e[0m\n"
      "\t--scroll[=<ms>]       : Scroll horizontally (optionally: delay ms "
      "(60)).\n"
      "\t--delta-move=<dx:dy>  : delta x and delta y when scrolling "
      "(default:1:0)\n"

      "\n  \e[1mFor Animations, Scrolling, or Video\e[0m\n"
      "  These options influence how long/often and what is shown.\n"
      "\t--loops=<num> : Number of runs through a full cycle. -1 means "
      "'forever'.\n"
      "\t                If not set, videos loop once, animated images "
      "forever\n"
      "\t                unless there is more than one file to show.\n"
      "\t--frames=<num>: Only show first num frames (if looping, loop only "
      "these)\n"
      "\t--frame-offset=<num>: Start animation/video at this frame\n"
      "\t-t<seconds>   : Stop after this time, independent of --loops or "
      "--frames\n",
      default_title ? "='" : "", default_title ? default_title : "",
      default_title ? "'" : "", width, height, kDefaultThreadCount, TV_VERSION);
  return (int)exit_code;
}

// Read list of filenames from newline separated file.
// Non-absolute files are resolved relative to the filelist_file or
// relative to currnet working directory.
bool AppendToFileList(const std::string &filelist_file,
                      bool consider_relative_to_filelist,
                      std::vector<std::string> *filelist) {
  std::ifstream filelist_stream(
      filelist_file == "-" ? "/dev/stdin" : filelist_file, std::ifstream::in);
  if (!filelist_stream) {
    fprintf(stderr, "%s: %s\n", filelist_file.c_str(), strerror(errno));
    return false;
  }
  const size_t last_slash = filelist_file.find_last_of('/');
  // Following works as expected if last_slash == npos (lsat_slash+1 == 0)
  const std::string prefix = filelist_file.substr(0, last_slash + 1);
  std::string filename;
  for (std::string filename; std::getline(filelist_stream, filename); /**/) {
    if (filename.empty())
      continue;
    if (consider_relative_to_filelist && //
        filename[0] != '/' && !prefix.empty()) {
      filename.insert(0, prefix);
    }
    filelist->push_back(filename);
  }
  return true;
}

static int PresentImages(LoadedImageSources *loaded_sources,
                         const tv::DisplayOptions &display_opts,
                         const tv::PresentationOptions &present,
                         tv::BufferedWriteSequencer *sequencer,
                         bool *any_animations_seen) {
  using tv::ThreadPool;
  std::unique_ptr<ThreadPool> compression_pool;
  std::unique_ptr<TerminalCanvas> canvas;
  switch (present.pixelation) {
  case Pixelation::kKittyGraphics:
    compression_pool.reset(new ThreadPool(sequencer->max_queue_len() + 1));
    canvas.reset(new KittyGraphicsCanvas(sequencer, compression_pool.get(),
                                         present.tmux_workaround,
                                         display_opts));
    break;
  case Pixelation::kiTerm2Graphics:
    compression_pool.reset(new ThreadPool(sequencer->max_queue_len() + 1));
    canvas.reset(new ITerm2GraphicsCanvas(sequencer, compression_pool.get(),
                                          display_opts));
    break;
#ifdef WITH_TV_SIXEL
  case Pixelation::kSixelGraphics:
    compression_pool.reset(new ThreadPool(sequencer->max_queue_len() + 1));
    canvas.reset(new tv::SixelCanvas(sequencer, compression_pool.get(),
                                     present.sixel_cursor_workaround,
                                     display_opts));
    break;
#endif
  case Pixelation::kHalfBlock:
  case Pixelation::kQuarterBlock:
  case Pixelation::kNotChosen: // Should not happen.
    canvas.reset(new UnicodeBlockCanvas(
        sequencer, present.pixelation == Pixelation::kQuarterBlock,
        present.terminal_use_upper_block, present.use_256_color));
  }

  auto renderer = tv::Renderer::Create(
      canvas.get(), display_opts, present.grid_cols, present.grid_rows,
      present.duration_between_images, present.duration_for_row);

  // Things to do before and after we show an image. Our goal is to keep
  // the terminal always in a good state (cursor on!) while also reacting
  // to Ctrl-C or terminations.
  // While showing an image we switch off the cursor but also arm the
  // signal handler to intercept and have a chance to bring terminal output
  // to a controlled stop.
  // Between showing images we _do_ want the default signal handler to be
  // active so that we can interrupt picture loading (because the internals
  // of the image loading libraries don' know about "interrupt_received".
  auto before_image_show = [present, &canvas](bool first) {
    signal(SIGTERM, InterruptHandler);
    signal(SIGINT, InterruptHandler);
    if (present.hide_cursor)
      canvas->CursorOff();
    if ((present.clear_screen == ClearScreen::kBeforeFirstImage && first) ||
        (present.clear_screen == ClearScreen::kBeforeEachImage)) {
      canvas->ClearScreen();
    }
  };

  auto after_image_show = [present, &canvas]() {
    if (present.hide_cursor)
      canvas->CursorOn();
    signal(SIGTERM, SIG_DFL);
    signal(SIGINT, SIG_DFL);
  };

  // Showing them in order of files on the command line.
  bool is_first = true;
  int valid_images = 0;
  for (auto &source_future : *loaded_sources) {
    if (interrupt_received)
      break;
    std::unique_ptr<tv::ImageSource> source(source_future.get());
    if (!source)
      continue;
    valid_images++;
    *any_animations_seen |= source->IsAnimationBeforeFrameLimit();
    before_image_show(is_first);
    source->SendFrames(
        present.duration_per_image, present.loops, interrupt_received,
        renderer->render_cb(source->FormatTitle(display_opts.title_format)));
    after_image_show();
    renderer->MaybeWaitBetweenImageSources();
    is_first = false;
  }
  sequencer->Flush();
  return valid_images;
}

static std::optional<Pixelation> ParsePixelation(const char *as_text) {
  if (!as_text)
    return std::nullopt;
  switch (tolower(as_text[0])) {
  case 'h':
    return Pixelation::kHalfBlock;
    break;
  case 'q':
    return Pixelation::kQuarterBlock;
    break;
  case 'k':
    return Pixelation::kKittyGraphics;
    break;
  case 'i':
    return Pixelation::kiTerm2Graphics;
    break;
#ifdef WITH_TV_SIXEL
  case 's':
    return Pixelation::kSixelGraphics;
    break;
#endif
  default:
    return std::nullopt;
  }
}

static const char *PixelationToString(Pixelation p) {
  switch (p) {
  case Pixelation::kHalfBlock:
    return "half block";
    break;
  case Pixelation::kQuarterBlock:
    return "quarter block";
    break;
  case Pixelation::kKittyGraphics:
    return "kitty graphics";
    break;
  case Pixelation::kiTerm2Graphics:
    return "iterm2 graphics";
    break;
#ifdef WITH_TV_SIXEL
  case Pixelation::kSixelGraphics:
    return "sixel graphics";
    break;
#endif
  case Pixelation::kNotChosen:
    return "(none)";
    break;
  }
  return ""; // Make compiler happy.
}

// Print our version and various version numbers from our dependencies.
static int PrintVersion(FILE *stream) {
  fprintf(stream, "tv " TV_VERSION " gitee.com/nidielaila/tv\n"
                  "Copyright (c) 2016..2024 Henner Zeller. "
                  "This program is free software; license GPL 2.0.\n\n");
#ifdef WITH_TV_GRPAPHICSMAGICK
  fprintf(stream, "Image decoding %s\n",
          tv::GraphicsMagickSource::VersionInfo());
#endif
#ifdef WITH_TV_OPENSLIDE_SUPPORT
  fprintf(stream, "Openslide %s\n", tv::OpenSlideSource::VersionInfo());
#endif
#ifdef WITH_TV_JPEG
  fprintf(stream, "Turbo JPEG\n");
#endif
#ifdef WITH_TV_RSVG
  fprintf(stream, "librsvg %d.%d.%d + cairo %d.%d.%d\n", LIBRSVG_MAJOR_VERSION,
          LIBRSVG_MINOR_VERSION, LIBRSVG_MICRO_VERSION, CAIRO_VERSION_MAJOR,
          CAIRO_VERSION_MINOR, CAIRO_VERSION_MICRO);
#endif
#ifdef WITH_TV_POPPLER
  fprintf(stream, "PDF rendering with poppler %s + cairo %d.%d.%d",
          poppler_get_version(), CAIRO_VERSION_MAJOR, CAIRO_VERSION_MINOR,
          CAIRO_VERSION_MICRO);
#if not POPPLER_CHECK_VERSION(0, 88, 0)
  // Too old versions of poppler don't have a bounding-box function
  fprintf(stream, " (no --auto-crop)");
#endif
  fprintf(stream, "\n");
#endif
#ifdef WITH_TV_QOI
  fprintf(stream, "QOI image loading\n");
#endif
#ifdef WITH_TV_STB
  fprintf(stream, "STB image loading; STB resize v"
#ifdef STB_RESIZE_VERSION2
                  "2"
#else
                  "1"
#endif
#ifdef WITH_TV_GRPAPHICSMAGICK
                  // If we have graphics magic, that will take images first,
                  // so STB will only really be called as fallback.
                  " (fallback)"
#endif
                  "\n");
#endif
  fprintf(stream, "swscale %s\n", AV_STRINGIFY(LIBSWSCALE_VERSION));
#ifdef WITH_TV_VIDEO
  fprintf(stream, "Video decoding %s\n", tv::VideoSource::VersionInfo());
#endif
#ifdef WITH_TV_SIXEL
  fprintf(stream, "Libsixel version %s\n", LIBSIXEL_VERSION);
#endif
  fprintf(stream, "Half, quarter, iterm2, and kitty graphics output: "
                  "tv builtin.\n");
  return 0;
}
